﻿/**
 * author:  Martin Hrabos
 * year:    2016
 * email:   hrabosmartin@gmail.com
 * 
 * FIT Brno University of Technology / ASCR Brno
 */

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using signalViewer;
using System.Threading;
using System.Diagnostics;
using System.IO;

namespace CorrCL
{
    public partial class SVP_CorrCL : UserControl
    {
        List<signalViewer.graphPanel> linkedChannels = new List<signalViewer.graphPanel>(); //--array of linked channels


        private int width;
        private int height;
        Bitmap bitmap;

        ManualResetEvent COMRE = new ManualResetEvent(false); //---manual reset event. Important when this plugin generates a COMMAND too. Otherwise this can be deleted.

        float bgwPercentDone = 0;

        mainView mv; //-- instance of signal viewer program

        Boolean gpuEnabled;
        Boolean cudaEnabled;
        int platformID = -1;
        Computation c = null;
        public SVP_CorrCL()
        {
            InitializeComponent();


            refrControls();

            refreshPlatforms();


        }
        // Function for enabling combobox with devices
        private void enableDevices() {
            object[] platforms = Computation.getPlatforms();
            comboBox1.Enabled = true;
            comboBox1.Items.Clear();
            comboBox1.Items.AddRange(platforms);
            comboBox1.SelectedIndex = 0;
            radioButton2.Select();
            gpuEnabled = true;
        }

        // Function for disabling combobox with devices
        private void disableDevices()
        {
            comboBox1.Enabled = false;
            comboBox1.Items.Clear();
            comboBox1.Items.Add("NO COMPATIBLE DEVICE");
            comboBox1.SelectedIndex = 0;
            radioButton2.Enabled = false;
            radioButton1.Select();
            gpuEnabled = false;
        }

        // Function for disabling gpu computations
        private void disableGpu()
        {
            comboBox1.Enabled = false;
            gpuEnabled = false;
        }

        // Function for refreshing platform devices
        private void refreshPlatforms()
        {


            try
            {
                object[] platforms = Computation.getPlatforms();
                if (platforms.Length > 0)
                {
                    enableDevices();
                }
                else
                {
                    disableDevices();
                }

                if (Computation.checkCUDA())
                    groupBox2.Enabled = true;
                else
                    groupBox2.Enabled = false;
            } catch (Exception e)
            {
                // OpenCL is not supported
                radioButton1.Checked = true;
                radioButton2.Enabled = false;
                groupBox2.Enabled = false;
                comboBox1.Enabled = false;
            }
        }

        public string getDeveloperInfo()
        {
            return "Martin Hrabos, developer, 2016"; //----change to your name
        }

        public string getDescription()
        {
            return "CorrCL"; //----enter description for your plugin
        }

        public string getCategory()
        {
            return "Analysis"; //---set category in plugins menu. If it does not exists, new category will be created
        }


        public string getName()
        {
            return "Accelerated Correlation";        //---plugin name, visible in Plugins menu
        }

        public void doExternalRefresh()
        {
            /*
             *  This is calle from signalViewer, when doing RefreshAllPluginForms()
             */

            refrControls();
            doPreviewWork();

        }


        public void presetBeforeLoaded()
        {
            /*
          This funciotn is called after user clicks on preset in menu, but before corresponding controls are filled with new values. 
           */
        }

        public void presetLoaded()
        {
            /*
             * This funciotn is called after user clicks on preset in menu. Corresponding controls (with filled TAG field) receives values from preset
             * and here you have to set values from controls to corresponding fields
            */

            refrControls();
        }


        /*
         //--When you need to have a command for scripts too you have to implement these two functions
         * 
         public string COMMAND_NAME(string parameters) //---change to COMMAND_SOMETHING if you need to name new command as "SOMETHING".
         {
             string pars = parameters.Substring(10);

             linkedChannels.Clear();

            

             string linkedChannelsFilter = "*";

             if (pars.IndexOf("CHANNEL(") > 0)
             {

                 int posAbbrev = pars.IndexOf(")");

                 int start = pars.IndexOf("CHANNEL(") + 8;

                 string sub1 = pars.Substring(start, posAbbrev-start+1);

                 linkedChannelsFilter = sub1.Substring(0, sub1.Length - 1);
             }

            

             mainView.footerMessage = "ShapeFinder started";


             bgw.RunWorkerAsync();

             COMRE.WaitOne();

             linkedChannels = null;
             GC.Collect();
             return "Done";
         }

         public static string CMDDESCRIPTION_NAME() //---change NAME to same as in the funcion before
         {
             return "FIND ANOTATIONS FOR CHALLENGE 2014";
         }

       */

        // Function for refreshing GUI controls
        private void refrControls()
        {
            /*
             * This function is called for refreshing a plugin form. 
             
             * */

            btChannels.Text = "";
            if (linkedChannels.Count == 0) btChannels.Text = "Drag a channel here or click";
            if (linkedChannels.Count == 1) btChannels.Text = "Channel : " + linkedChannels[0].channelName;
            if (linkedChannels.Count > 1) btChannels.Text = linkedChannels.Count.ToString() + " channels are choosed";

            btProcess.Enabled = linkedChannels.Count > 1;

            if (bgw.IsBusy && bgw.CancellationPending) btProcess.Text = "Wait for cancellation";
            if (bgw.IsBusy && !bgw.CancellationPending) btProcess.Text = "Cancel";
            if (!bgw.IsBusy) btProcess.Text = "Process";


/*
            if (marksList != null && marksList.Count > 0) btMarks.Text = marksList.Count + " marks selected";
            else btMarks.Text = "(Choose some marks)";*/

            pbx.Refresh();

        }

        private void doPreviewWork()
        {
            /*This function creates anything for preview. Not neccesary to implement
             * */
            pbx.Refresh();
        }

        private void btChannels_DragDrop(object sender, DragEventArgs e)
        {
            /*
             *This method add dragged channel to linkedchannels
             */
            string s = e.Data.GetData(DataFormats.Text).ToString();

            try
            {
                linkedChannels.Add(mv.getGPbyName(s));
                float freq = mainView.sampleFrequency;


                refrControls();
                doPreviewWork();

            }
            catch (Exception exp)
            {
                mainView.log(exp, "Error while drag&drop", this); // this line will log error into "errorlog.txt"
                MessageBox.Show("Error:" + exp.Message);
            }
        }

        private void btChannels_DragEnter(object sender, DragEventArgs e)
        {
            string s = e.Data.GetData(DataFormats.Text).ToString();

            if (mv.getGPbyName(s) != null)
            {
                e.Effect = DragDropEffects.Copy;
            }
            else
                e.Effect = DragDropEffects.None;
        }

        private void btChannels_Click(object sender, EventArgs e)
        {

            signalViewer.selectChannelForm sc = new signalViewer.selectChannelForm();
            sc.regenerateList(linkedChannels);

            if (sc.ShowDialog() == DialogResult.OK)
            {
                linkedChannels.Clear();
                for (int i = 0; i < sc.lv.SelectedItems.Count; i++)
                {
                    linkedChannels.Add(mv.getGPbyName(sc.lv.SelectedItems[i].Text));
                }
                float freq = mainView.sampleFrequency;

                if (gpuEnabled)
                { 

                    long signalLength = linkedChannels[0].dataCache[0].data.Count;
                    long maxObjectSize = Computation.getMaxDevObjectMemoryAllocationSize(comboBox1.SelectedIndex);
                    
                    // Set max time shift. 1/4 of device memory size or 1/10 of signal length (smaller value is set) 
                    numericUpDown1.Maximum = (maxObjectSize < signalLength ? maxObjectSize : signalLength / (decimal)10) / (decimal)freq;
                }
                refrControls();
                doPreviewWork();
            }
        }

        // Click on Process button
        private void btProcess_Click(object sender, EventArgs e)
        {

            try
            {
                if (linkedChannels[0].dataCache[0].data.Count * sizeof(float) > Computation.getMaxDevObjectMemoryAllocationSize(comboBox1.SelectedIndex))
                {
                    
                    MessageBox.Show("Not enough memory on GPU. Try shorter signals", "Memory Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return;
                }

            }
            catch
            {
                MessageBox.Show("There was an error initializing OpenCL", "GPU Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }

            if (!bgw.IsBusy)
            {

                if (numericUpDown3.Value <= 0)
                {
                    MessageBox.Show("Window size cannot be 0!", "Parameter Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return;
                }
                if (numericUpDown4.Value <= 0)
                {
                    MessageBox.Show("Window offset cannot be 0!", "Parameter Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return;
                }
                if (numericUpDown1.Value > numericUpDown3.Value / 2)
                {
                    MessageBox.Show("Time delay must be max 1/2 of window Size", "Parameter Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return;
                }

                progressBar1.Visible = true;
                topPanel.Enabled = false;
                button1.Enabled = false;
                button3.Enabled = false;


                // Run computations
                if (gpuEnabled)
                    platformID = comboBox1.SelectedIndex;
                else
                    platformID = -1;

                bgw.RunWorkerAsync();
            }
            else
            { 
                // Cancel running comuptation
                bgw.CancelAsync();
            }

            //refrControls();
        }


        private void bgw_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            bgwPercentDone = e.ProgressPercentage;
            progressBar1.Value = e.ProgressPercentage;
            label1.Text = (string)e.UserState;
            pbx.Refresh();
            if (bgw.IsBusy && bgw.CancellationPending) btProcess.Text = "Wait for cancellation";
            if (bgw.IsBusy && !bgw.CancellationPending) btProcess.Text = "Cancel";

        }

        // Function for drawing results
        private void drawResultMatrix(PaintEventArgs e, Graphics g)
        {
            if (c != null)
            {
                width = pbx.Width;
                height = pbx.Height;
                ResultMatrix rm = c.getResults();
                for (int j = 1; j < rm.labels.Count; j++)
                {
                    e.Graphics.DrawString(rm.labels[j], SystemFonts.DefaultFont, Brushes.Black,
                         ResultPoint.MARGIN_LEFT,
                         (j - 1) * (ResultPoint.RECT_HEIGHT + ResultPoint.RECT_PADDING) + ResultPoint.MARGIN_TOP + rm.LONGEST_LABEL_LENGTH + ((ResultPoint.RECT_HEIGHT / 2) - 8));
                    g.DrawString(rm.labels[j], SystemFonts.DefaultFont, Brushes.Black,
                         ResultPoint.MARGIN_LEFT,
                         (j - 1) * (ResultPoint.RECT_HEIGHT + ResultPoint.RECT_PADDING) + ResultPoint.MARGIN_TOP + rm.LONGEST_LABEL_LENGTH + ((ResultPoint.RECT_HEIGHT / 2) - 8));
                }

                for (int i = 1; i < rm.labels.Count; i++)
                {
                    // Draw Labels for first column
                    StringFormat verticalFormat = new StringFormat(StringFormatFlags.DirectionVertical);
                    e.Graphics.DrawString(rm.labels[i - 1], SystemFonts.DefaultFont, Brushes.Black,
                        (i - 1) * (ResultPoint.RECT_WIDTH + ResultPoint.RECT_PADDING) + ResultPoint.MARGIN_LEFT + rm.LONGEST_LABEL_LENGTH + ((ResultPoint.RECT_WIDTH / 2) - 8),
                        ResultPoint.MARGIN_TOP, verticalFormat);
                    g.DrawString(rm.labels[i - 1], SystemFonts.DefaultFont, Brushes.Black,
                        (i - 1) * (ResultPoint.RECT_WIDTH + ResultPoint.RECT_PADDING) + ResultPoint.MARGIN_LEFT + rm.LONGEST_LABEL_LENGTH + ((ResultPoint.RECT_WIDTH / 2) - 8),
                        ResultPoint.MARGIN_TOP, verticalFormat);
                }


                foreach (ResultPoint rp in rm.results)
                {
                    Rectangle rect = rp.getRectangle();

                    if (rect.X > width - 50)
                    {
                        this.width += ResultPoint.RECT_WIDTH + ResultPoint.RECT_PADDING;
                        pbx.Size = new Size(width, height);
                    }
                    if (rect.Y > height - 50)
                    {
                        this.height += ResultPoint.RECT_HEIGHT + ResultPoint.RECT_PADDING;
                        pbx.Size = new Size(width, height);
                    }

                    e.Graphics.FillRectangle(new SolidBrush(rp.getColor()), rect);
                    g.FillRectangle(new SolidBrush(rp.getColor()), rect);
                }

                // Draw legend
                int step = 0;
                for (float i=1; i>=-1; i-=0.2f)
                {
                    Rectangle rect = new Rectangle(width-180, 50+step*10, 25, 10);
                    e.Graphics.FillRectangle(new SolidBrush(ResultPoint.calculateColor(i)), rect);
                    g.FillRectangle(new SolidBrush(ResultPoint.calculateColor(i)), rect);
                    step++;
                }

                e.Graphics.DrawString("Strong positive correlation", SystemFonts.DefaultFont, Brushes.Black, width-150, 48);
                e.Graphics.DrawString("No correlation", SystemFonts.DefaultFont, Brushes.Black, width-150, 98);
                e.Graphics.DrawString("Strong negative correlation", SystemFonts.DefaultFont, Brushes.Black, width-150, 148);

                g.DrawString("Strong positive correlation", SystemFonts.DefaultFont, Brushes.Black, width - 150, 48);
                g.DrawString("No correlation", SystemFonts.DefaultFont, Brushes.Black, width - 150, 98);
                g.DrawString("Strong negative correlation", SystemFonts.DefaultFont, Brushes.Black, width - 150, 148);

            }
        }

        private void pbx_Paint(object sender, PaintEventArgs e)
        {
            bitmap = new Bitmap(pbx.Width, pbx.Height);
            Graphics g = Graphics.FromImage(bitmap);
            g.Clear(Color.White);


            if ((linkedChannels == null || linkedChannels.Count < 2) && c == null)
            {
                e.Graphics.DrawString("Please attach at least 2 channels first", SystemFonts.DefaultFont, Brushes.Black, pbx.Width / 2, pbx.Height / 2, mainView.sfc);
                g.DrawString("Please attach at least 2 channels first", SystemFonts.DefaultFont, Brushes.Black, pbx.Width / 2, pbx.Height / 2, mainView.sfc);
                button2.Enabled = false;
                return;
            } else
            {
                if (c == null)
                {
                    e.Graphics.DrawString("Select computation device, set time delay and click \"Process\".", SystemFonts.DefaultFont, Brushes.Black, pbx.Width / 2, pbx.Height / 2, mainView.sfc);
                    g.DrawString("Select computation device, set time delay and click \"Process\".", SystemFonts.DefaultFont, Brushes.Black, pbx.Width / 2, pbx.Height / 2, mainView.sfc);
                }
            }



            if (bgw.IsBusy || c != null)
            {
                button2.Enabled = false;
                drawResultMatrix(e, g);                
            }

            if (!bgw.IsBusy && c != null)
            {
                drawResultMatrix(e, g);
                button2.Enabled = true;
            }

        }

        // Function for running backgroundWorker with computations
        private void bgw_DoWork(object sender, DoWorkEventArgs e)
        {


            List<List<float>> data = new List<List<float>>();
            List<string> labels = new List<string>();
            for (int i=0; i<linkedChannels.Count; i++)
            {

                data.Add(linkedChannels[i].dataCache[0].data);
                labels.Add(linkedChannels[i].channelName);
            }


            if (gpuEnabled)
            {
                c = new GpuComputation(platformID);
                c.frequency = mainView.sampleFrequency;
                c.windowSize = (long)((float)numericUpDown3.Value * mainView.sampleFrequency);
                c.windowOffset = (long)((float)numericUpDown4.Value * mainView.sampleFrequency);
                c.sourceFile = Path.GetFileName(mainView.currentFile);
                c.setData(data, labels);
                c.setOffset((long)((float)numericUpDown1.Value*mainView.sampleFrequency));
                c.ProgressChanged += (s, pe) => bgw.ReportProgress(pe.ProgressPercentage, pe.UserState);
                c.cudaSelected = radioButton4.Checked;
                c.compute();
                
            }
            else
            {
                c = new CpuComputation();
                c.frequency = mainView.sampleFrequency;
                c.windowSize = (long)((float)numericUpDown3.Value * mainView.sampleFrequency);
                c.windowOffset = (long)((float)numericUpDown4.Value * mainView.sampleFrequency);
                c.sourceFile = Path.GetFileName(mainView.currentFile);
                c.setData(data, labels);
                c.THREADS_COUNT = ((int)numericUpDown2.Value);
                c.setOffset((long)((float)numericUpDown1.Value * mainView.sampleFrequency));
                c.ProgressChanged += (s, pe) => bgw.ReportProgress(pe.ProgressPercentage, pe.UserState);  
                c.compute();
            }

            /*for (int i = 0; i < linkedChannels[0].dataCache[0].data.Count; i++)
            {



                if (i % 10000 == 0)
                    bgw.ReportProgress(50); //---report progress at least time to time
            }

            result = new Object(); //---only illustrative example of result

            COMRE.Set(); //---this is important only when implementing a command*/
        }


        // Doing job after backgroundWorker finishes
        private void bgw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            refrControls();
            button2.Enabled = true;
            progressBar1.Visible = false;
            topPanel.Enabled = true;
            button1.Enabled = true;
            button3.Enabled = true;
        }


        private void SVP_plugin_v3_Load(object sender, EventArgs e)
        {
            if (this.ParentForm != null)
                mv = (mainView)Application.OpenForms[0]; //--this links mv property to signalplant application
        }


        // Functions for switching between CPU/GPU
        public void radioButtons_CheckedChanged(object sender, EventArgs e)
        {

            RadioButton radioButton = sender as RadioButton;
            if (radioButton1.Checked)
            {
                comboBox1.Visible = false;
                numericUpDown2.Visible = true;
                label2.Text = "Number of threads:";
                disableGpu();
                groupBox2.Visible = false;
            } else
            {
                numericUpDown2.Visible = false;
                comboBox1.Visible = true;
                label2.Text = "GPU Device:";
                enableDevices();
                groupBox2.Visible = true;
                groupBox2.Enabled = Computation.checkCUDA();
            }
        }

        // Functions for detecting clicks on rectangles
        private void pbx_MouseDown(object sender, MouseEventArgs e)
        {

            Debug.WriteLine(e.Location.X + " "+  e.Location.Y);
            if (c != null)
            {
                ResultPoint resPoint = c.getResults().getResultPointbyClick(e.Location.X, e.Location.Y);
                if (resPoint != null)
                {
                    Form f = new SignalPlotter(resPoint, c.getResults().frequency, c.getResults().windowSize, c.getResults().windowOffset, c.getResults().sourceFile);
                    float[] values = resPoint.getSignal();
                    
                    f.Show();
                }

            }
            refrControls();
        }

        // Function for listening window resize events
        private void SVP_CorrCL_Resize(object sender, EventArgs e)
        {
            Control control = (Control)sender;
            this.width = control.Size.Width;
            this.height = control.Size.Height;
            pbx.Size = new Size(width, height-100);
            refrControls();
        }

        // Function for opening files
        private void button1_Click(object sender, EventArgs e)
        {
            OpenFileDialog dialog = new OpenFileDialog();
            dialog.Title = "Browse HDF5 Files";
            dialog.CheckFileExists = true;
            dialog.CheckPathExists = true;
            dialog.DefaultExt = "h5";
            dialog.Filter = "HDF5 files (*.h5) | *.h5";
            dialog.FilterIndex = 0;
            dialog.RestoreDirectory = true;

            if (dialog.ShowDialog() == DialogResult.OK) {
                if (c == null)
                    c = new GpuComputation(platformID);
                SignalFileWriter h = new SignalFileWriter(dialog.FileName);
                ResultMatrix rm = h.ReadH5();
                if (rm != null)
                {
                    c.setResults(h.ReadH5());
                    refrControls();
                    button2.Enabled = true;
                }
            }
        }

        // Function for saving files
        private void button2_Click(object sender, EventArgs e)
        {

            SaveFileDialog dialog = new SaveFileDialog();
            dialog.Filter = "HDF5 files (*.h5) | *.h5| CSV files (*.csv) | *.csv";
            dialog.FilterIndex = 0;
            dialog.RestoreDirectory = true;

            if (dialog.ShowDialog() == DialogResult.OK)
            {
                
                string ext = Path.GetExtension(dialog.FileName);
                Debug.WriteLine(ext.ToLower());
                if (ext.ToLower() == ".h5")
                {
                    SignalFileWriter h = new SignalFileWriter(dialog.FileName);
                    //h.WriteH5(c.getResults(), c.getResults().frequency);
                    h.WriteH5(c.getResults(), c.getResults().frequency, c.getResults().windowSize, c.getResults().windowOffset);
                } else if (ext.ToLower() == ".csv")
                {
                    SignalFileWriter h = new SignalFileWriter(dialog.FileName);
                    h.WriteCSV(c.getResults(), c.getResults().frequency);
                }
            }

        }

        // Function for copying results to clipboard
        private void button3_Click(object sender, EventArgs e)
        {
            Clipboard.SetDataObject(bitmap);
        }

        // Function for switching between CUDA/OpenCL
        private void radioButton3_CheckedChanged(object sender, EventArgs e)
        {
            RadioButton radioButton = sender as RadioButton;

            // OpenCL
            if (radioButton3.Checked)
            {
                cudaEnabled = false;
                comboBox1.Enabled = true;
            }
            // CUDA
            else if (radioButton4.Checked)
            {
                cudaEnabled = true;
                comboBox1.Enabled = false;
            }
        }
    }
}
